package org.danger.dy.common.web.logging;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.danger.dy.common.lang.utils.JsonUtils;
import com.danger.dy.common.lang.utils.Lang;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.danger.t7.common.spring.SpringContext;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

@Slf4j
@Order
public class DyLoggingFilter extends OncePerRequestFilter {

    private static final String REQUEST_ID = "requestId";

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        if (!needLog(request)
                || request.getMethod().equalsIgnoreCase("options")) {
            filterChain.doFilter(request, response);
            return;
        }

        if (!(response instanceof ContentCachingResponseWrapper)) {
            response = new ContentCachingResponseWrapper(response);
        }

        String requestId = Lang.fastUUID();

        MDC.put(REQUEST_ID, requestId);
        try {
            response.setHeader(REQUEST_ID, requestId);

            long startTime = System.currentTimeMillis();
            LoggingRequest loggingRequest = getLoggingRequest(request);

            filterChain.doFilter(request, response);

            copyBodyToResponse(response);
            long cost = System.currentTimeMillis() - startTime;

            log(">>>[request finished: {}, from {}](耗时{}ms) params={}, body={}, response status={}",
                    request.getRequestURI(),
                    loggingRequest.getClientIp(),
                    cost, JsonUtils.toJson(loggingRequest.getParams()),
                    JsonUtils.toJson(loggingRequest.getBody()), response.getStatus()
            );

        } finally {
            MDC.clear();
        }
    }

    private void log(String format, Object... arguments) {
        try {
            Environment env = SpringContext.getBean(Environment.class);
            boolean logEnable = env.getProperty("common.log.enable", Boolean.class, true);
            String logLevel = env.getProperty("common.log.level", String.class, "warn");
            if (!logEnable) {
                return;
            }
            if (logLevel.endsWith("warn")) {
                log.warn(format, arguments);
            } else {
                log.info(format, arguments);
            }
        } catch (Exception e) {
            //ignore
        }
    }

    private boolean needLog(HttpServletRequest request) {
        if (request.getRequestURI().contains(".")) {
            return false;
        }
        if (request.getRequestURI().contains("actuator")
                || request.getRequestURI().contains("health")) {
            return false;
        }
        return true;
    }

    private LoggingRequest getLoggingRequest(HttpServletRequest request) {

        Map<String, String> headers = getHeaders(request);
        LoggingRequest loggingRequest = new LoggingRequest();
        loggingRequest.setClientIp(request.getRemoteHost());
        loggingRequest.setMethod(request.getMethod());
        loggingRequest.setPath(request.getRequestURI());
        loggingRequest.setParams(getParameters(request.getParameterMap()));
        loggingRequest.setHeaders(headers);

        if (request.getMethod().equalsIgnoreCase("post")
                || request.getMethod().equalsIgnoreCase("put")) {
            if (request.getContentType() != null && request.getContentType().contains("application/json")) {
                try {
                    BufferedReader reader = request.getReader();
                    StringBuilder sb = new StringBuilder();
                    String line = null;
                    while ((line = reader.readLine()) != null) {
                        sb.append(line + "\n");
                    }
                    loggingRequest.setBody(sb.toString());
                    reader.close();
                } catch (IOException e) {
                    log.error("error", e);
                }
            }
        }
        return loggingRequest;
    }

    /**
     * @param response .
     * @throws IOException .
     */
    private void copyBodyToResponse(HttpServletResponse response) throws IOException {
        ContentCachingResponseWrapper responseWrapper = WebUtils
                .getNativeResponse(response, ContentCachingResponseWrapper.class);
        Objects.requireNonNull(responseWrapper).copyBodyToResponse();
    }

    /**
     * @param parameterMap .
     * @return .
     */
    public Map<String, String> getParameters(Map<String, String[]> parameterMap) {
        return parameterMap.entrySet().stream().collect(Collectors.toMap(Entry::getKey, e -> {
            String[] values = e.getValue();
            return values.length > 0 ? String.join(",", values) : "[EMPTY]";
        }));
    }

    /**
     * @param request .
     * @return .
     */
    public Map<String, String> getHeaders(HttpServletRequest request) {
        Map<String, String> headers = new HashMap<>(0);
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            if (headerName != null) {
                headers.put(headerName, request.getHeader(headerName));
            }
        }
        return headers;
    }

    public Map<String, String> getHeaders(HttpServletResponse response) {
        Map<String, String> headers = new HashMap<>(0);
        for (String headerName : response.getHeaderNames()) {
            headers.put(headerName, response.getHeader(headerName));
        }
        return headers;
    }

    @Getter
    @Setter
    public static class LoggingRequest {
        private String clientIp;
        private String method;
        private String path;
        private Map<String, String> params;
        private Map<String, String> headers;
        private String body;
    }

    @Getter
    @Setter
    public static class LoggingResponse {
        private Map<String, String> headers;
        private String body;
    }
}
